// ValidationAspects
// Written by Mike Saunders
// Released under Microsoft Public License (Ms-PL)
using System;
using System.Linq;
using NUnit.Framework;
using ValidationAspects;
using ValidationAspects.Core;
using ValidationAspects.Exceptions;
using ValidationAspects.Factories;
using ValidationAspects.Sdk;

namespace TestValidationAspects.Core
{
	[TestFixture]
	public class TestValidationRegistry
	{
		private ValidationRegistry<object> _registry;

		[SetUp]
		public void SetUp()
		{
			_registry = new ValidationRegistry<object>(o => o.GetType(), o => Enumerable.Empty<IValidatorFactory>());
		}

		#region Test Constructor
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_Constructor_throws_for_null_selecTypeFunc()
		{
			new ValidationRegistry<object>(null, o => null);
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_Constructor_throws_for_null_factoryDiscoveryFunc()
		{
			new ValidationRegistry<object>(o => o.GetType(), null);
		}
		#endregion

		#region Test ClearCache
		[Test]
		public void Test_ClearCache()
		{
			object value = new object();
			MockValidatorFactory<object> factory = new MockValidatorFactory<object>();
			_registry.ReplaceValidation(value, new[] {factory});
			factory.Reset();

			IValidationContext context = new MockValidationContext();
			_registry.ResolveValidator(value).Invoke(value, context);

			factory.TestState(value, context);
			factory.Reset();

			_registry.ClearCache();

			Assert.IsNull(_registry.ResolveValidator(value));
		}
		#endregion

		#region Test GetFactories
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_GetFactories_throws_for_null_param()
		{
			_registry.GetFactories(null);
		}

		[Test]
		public void Test_GetFactories_calls_discoverFactories_func_on_first_call()
		{
			MockValidatorFactory<object> factory = new MockValidatorFactory<object>();
			ValidationRegistry<object> registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factory});

			IValidatorFactory[] factories = registry.GetFactories(new object());
			Assert.AreEqual(1, factories.Length);
			Assert.AreSame(factory, factories[0]);
		}

		[Test]
		public void Test_GetFactories_calls_discoverFactories_func_if_the_cache_is_cleared()
		{
			int callCount = 0;
			MockValidatorFactory<object> factory = new MockValidatorFactory<object>();
			ValidationRegistry<object> registry = new ValidationRegistry<object>(o => o.GetType(), o =>
			                                                                                       {
			                                                                                       	++callCount;
			                                                                                       	return new[] {factory};
			                                                                                       });

			object target = new object();

			IValidatorFactory[] factories = registry.GetFactories(target);
			Assert.AreEqual(1, callCount);
			Assert.AreEqual(1, factories.Length);
			Assert.AreSame(factory, factories[0]);

			registry.ClearCache();

			factories = registry.GetFactories(target);
			Assert.AreEqual(2, callCount);
			Assert.AreEqual(1, factories.Length);
			Assert.AreSame(factory, factories[0]);
		}

		[Test]
		public void Test_GetFactories_caches_discovered_factories()
		{
			int callCount = 0;
			MockValidatorFactory<object> factory = new MockValidatorFactory<object>();
			ValidationRegistry<object> registry = new ValidationRegistry<object>(o => o.GetType(), o =>
			                                                                                       {
			                                                                                       	++callCount;
			                                                                                       	return new[] {factory};
			                                                                                       });

			object target = new object();

			IValidatorFactory[] factories = registry.GetFactories(target);
			Assert.AreEqual(1, callCount);
			Assert.AreEqual(1, factories.Length);
			Assert.AreSame(factory, factories[0]);

			IValidatorFactory[] factories2 = registry.GetFactories(target);
			Assert.AreEqual(1, callCount);
			Assert.AreSame(factories, factories2);
		}
		#endregion

		#region Test RevertValidation
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_RevertValidation_throws_for_null_target_param()
		{
			_registry.RevertValidation(null);
		}

		[Test]
		public void Test_RevertValidation()
		{
			object value = new object();
			MockValidatorFactory<object> factoryA = new MockValidatorFactory<object>();
			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factoryA});

			MockValidatorFactory<object> factoryB = new MockValidatorFactory<object>();
			registry.ReplaceValidation(value, new[] {factoryB});

			factoryA.Reset();
			factoryB.Reset();

			IValidationContext context = new MockValidationContext();
			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(null, null);
			factoryB.TestState(value, context);

			factoryB.Reset();

			registry.RevertValidation(value);

			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);
			factoryB.TestState(null, null);
		}

		[Test]
		public void Test_RevertValidation_fails_fast_for_incompatible_ValidatorFactory()
		{
			IncompatibleFactory factory = new IncompatibleFactory();
			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factory});
			Test.Throws<UnsupportedValidationException>(() => registry.RevertValidation(new object()));
		}
		#endregion

		#region Test ReplaceValidation
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidation_throws_for_null_target_param()
		{
			_registry.ReplaceValidation(null, Enumerable.Empty<IValidatorFactory>());
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidation_throws_for_null_factories_param()
		{
			_registry.ReplaceValidation(new object(), null);
		}

		[Test]
		public void Test_ReplaceValidation()
		{
			object value = new object();
			MockValidatorFactory<object> factoryA = new MockValidatorFactory<object>();
			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factoryA});

			factoryA.Reset();

			IValidationContext context = new MockValidationContext();
			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);

			factoryA.Reset();

			MockValidatorFactory<object> factoryB = new MockValidatorFactory<object>();
			registry.ReplaceValidation(value, new[] {factoryB});

			factoryB.Reset();

			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(null, null);
			factoryB.TestState(value, context);

			factoryB.Reset();

			MockValidatorFactory<object> factoryC = new MockValidatorFactory<object>();
			registry.ReplaceValidation(value, new[] {factoryC});

			factoryC.Reset();

			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(null, null);
			factoryB.TestState(null, null);
			factoryC.TestState(value, context);
		}

		[Test]
		public void Test_ReplaceValidation_with_a_NoValidation_factory()
		{
			object value = new object();
			MockValidatorFactory<object> factoryA = new MockValidatorFactory<object>();
			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factoryA});

			factoryA.Reset();

			IValidationContext context = new MockValidationContext();
			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);

			NoValidation noValidation = new NoValidation();
			registry.ReplaceValidation(value, new[] {noValidation});

			Assert.IsNull(registry.ResolveValidator(value));
		}

		[Test]
		public void Test_ReplaceValidation_fails_fast_for_incompatible_ValidatorFactory()
		{
			Test.Throws<UnsupportedValidationException>(() => _registry.ReplaceValidation(new object(), new[] {new IncompatibleFactory()}));
		}
		#endregion

		#region Test ReplaceValidationT
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidationT_throws_for_null_target_param()
		{
			_registry.ReplaceValidation<string>(null, (v, c) => { });
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidationT_throws_for_null_action_param()
		{
			_registry.ReplaceValidation<string>(new object(), null);
		}

		[Test]
		public void Test_ReplaceValidationT()
		{
			object value = new object();
			MockValidatorFactory<object> factoryA = new MockValidatorFactory<object>();
			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factoryA});

			factoryA.Reset();

			IValidationContext instance = new MockValidationContext();
			registry.ResolveValidator(value).Invoke(value, instance);

			factoryA.TestState(value, instance);

			factoryA.Reset();

			object valueFromB = null;
			IValidationContext contextFromB = null;
			registry.ReplaceValidation<object>(value, (v, c) =>
			                                          {
			                                          	valueFromB = v;
			                                          	contextFromB = c;
			                                          });

			registry.ResolveValidator(value).Invoke(value, instance);

			factoryA.TestState(null, null);
			Assert.AreSame(value, valueFromB);
			Assert.AreSame(instance, contextFromB);

			factoryA.Reset();
			valueFromB = null;
			contextFromB = null;

			object valueFromC = null;
			IValidationContext contextFromC = null;
			registry.ReplaceValidation<object>(value, (v, c) =>
			                                          {
			                                          	valueFromC = v;
			                                          	contextFromC = c;
			                                          });

			registry.ResolveValidator(value).Invoke(value, instance);

			factoryA.TestState(null, null);
			Assert.AreSame(null, valueFromB);
			Assert.AreSame(null, contextFromB);
			Assert.AreSame(value, valueFromC);
			Assert.AreSame(instance, contextFromC);
		}

		[Test]
		public void Test_ReplaceValidationT_fails_fast_for_incompatible_ValidatorFactory()
		{
			Test.Throws<UnsupportedValidationException>(() => _registry.ReplaceValidation<double>(new object(), (v, c) => { }));
		}
		#endregion

		#region Test AddValidation
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidation_throws_for_null_target_param()
		{
			_registry.AddValidation(null, Enumerable.Empty<IValidatorFactory>());
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidation_throws_for_null_factories_param()
		{
			_registry.AddValidation(new object(), null);
		}

		[Test]
		public void Test_AddValidation()
		{
			object value = new object();
			MockValidatorFactory<object> factoryA = new MockValidatorFactory<object>();
			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factoryA});

			factoryA.Reset();

			IValidationContext context = new MockValidationContext();
			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);

			factoryA.Reset();

			MockValidatorFactory<object> factoryB = new MockValidatorFactory<object>();
			registry.AddValidation(value, new[] {factoryB});

			factoryB.Reset();

			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);
			factoryB.TestState(value, context);

			factoryA.Reset();
			factoryB.Reset();

			MockValidatorFactory<object> factoryC = new MockValidatorFactory<object>();
			registry.AddValidation(value, new[] {factoryC});

			factoryC.Reset();

			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);
			factoryB.TestState(value, context);
			factoryC.TestState(value, context);
		}

		[Test]
		public void Test_AddValidation_fails_fast_for_incompatible_ValidatorFactory()
		{
			Test.Throws<UnsupportedValidationException>(() => _registry.AddValidation(new object(), new[] {new IncompatibleFactory()}));
		}
		#endregion

		#region Test AddValidationT
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidationT_throws_for_null_target_param()
		{
			_registry.AddValidation<string>(null, (v, c) => { });
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidationT_throws_for_null_action_param()
		{
			_registry.AddValidation<string>(new object(), null);
		}

		[Test]
		public void Test_AddValidationT()
		{
			object value = new object();
			MockValidatorFactory<object> factoryA = new MockValidatorFactory<object>();
			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factoryA});

			factoryA.Reset();

			IValidationContext context = new MockValidationContext();
			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);

			factoryA.Reset();

			object valueFromB = null;
			IValidationContext contextFromB = null;
			registry.AddValidation<object>(value, (v, c) =>
			                                      {
			                                      	valueFromB = v;
			                                      	contextFromB = c;
			                                      });

			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);
			Assert.AreSame(value, valueFromB);
			Assert.AreSame(context, contextFromB);

			factoryA.Reset();
			valueFromB = null;
			contextFromB = null;

			object valueFromC = null;
			IValidationContext contextFromC = null;
			registry.AddValidation<object>(value, (v, c) =>
			                                      {
			                                      	valueFromC = v;
			                                      	contextFromC = c;
			                                      });

			registry.ResolveValidator(value).Invoke(value, context);

			factoryA.TestState(value, context);
			Assert.AreSame(value, valueFromB);
			Assert.AreSame(context, contextFromB);
			Assert.AreSame(value, valueFromC);
			Assert.AreSame(context, contextFromC);
		}

		[Test]
		public void Test_AddValidationT_fails_fast_for_incompatible_ValidatorFactory()
		{
			Test.Throws<UnsupportedValidationException>(() => _registry.AddValidation<double>(new object(), (v, c) => { }));
		}
		#endregion

		#region Test ResolveValidator
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ResolveValidator_throws_for_null_target_param()
		{
			_registry.ResolveValidator(null);
		}

		[Test]
		public void Test_ResolveValidator_uses_a_cache()
		{
			typeof(object).ReplaceValidation(new[] {new MockValidatorFactory<object>()});
			object target = new object();

			IValidator validator = _registry.ResolveValidator(target);
			Assert.AreSame(validator, _registry.ResolveValidator(target));
		}

		[Test]
		public void Test_ResolveValidator_creates_null_when_no_factories_are_registered_or_discovered()
		{
			Assert.IsNull(_registry.ResolveValidator(new object()));
		}

		[Test]
		public void Test_ResolveValidator_creates_a_validator_from_the_discovered_factories_when_no_factories_are_registered()
		{
			object value = new object();
			MockValidatorFactory<object> factory = new MockValidatorFactory<object>();

			var registry = new ValidationRegistry<object>(o => o.GetType(), o => new[] {factory});

			factory.TestState(null, null);

			IValidationContext context = new MockValidationContext();
			registry.ResolveValidator(value).Invoke(value, context);

			factory.TestState(value, context);
		}

		[Test]
		public void Test_ResolveValidator_throws_if_validator_creation_fails()
		{
			Test.Throws<ValidationException, NotImplementedException>(() => _registry.ReplaceValidation(new object(), new[] {new ThrowingFactory()}),
			                                                          string.Format("Failed to construct validator for [type:{0}].", typeof(object)));
		}
		#endregion

		#region Mocks
		public class IncompatibleFactory : ValidatorFactoryBase<double>
		{
			#region Overrides of ValidatorFactoryBase<double>
			protected override Action<double, IValidationContext> ValidateAction
			{
				get { return (v, c) => { }; }
			}
			#endregion
		}

		public class ThrowingFactory : IValidatorFactory
		{
			#region Implementation of IValidatorFactory
			public IValidator<T> Create<T>()
			{
				throw new NotImplementedException();
			}
			#endregion
		}

		public class Derived : Base {}

		public class Base {}

		public class Unknown {}
		#endregion
	}
}